﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.HomeServer.SDK.Interop.v1;
using System.Net.NetworkInformation;
using System.Net;
using System.Threading;
using RouterControl.Whs;

namespace RouterControl.Service.DeviceSlaves
{
    [RouterSlave]
    public sealed class RouterHijackingSlave : RouterSlave
    {
        private volatile CommonInternetGateway _router;
        private readonly object _checkPresentationLock = new object();
        private Whs.WhsNotification _hijackedNotification;
        private Whs.WhsNotification _unsupportedNotification;
        private Timer _checkTimer;
        private DateTime? _lastWorking;
        private Uri _routerUrl;
        private string _routerHost;

        /* seperate timer is used to check
        public override void OnDataUpdated()
        {
            base.OnDataUpdated();

            // continue
            try
            {
                CheckPresentation();
            }
            catch (Exception x)
            {
                _log.Warn("Failed to check router presentation layer", x);

                throw;
            }
        }*/

        public override void OnRouterChanged(CommonInternetGateway newRouter)
        {
            base.OnRouterChanged(newRouter);

            _router = null;

            // handle change
            _lastWorking = null;
            _routerHost = null;
            _routerUrl = null;
            _router = newRouter;

            // try to determine host
            try
            {
                var h = _router.Device.PresentationURL;

                if (h != null)
                {
                    // update router information
                    _routerHost = h.Host;
                    _routerUrl = h;         // do not set before extracting all information which might cause an error

                    // handle developer debug stuff
                    if (Properties.Settings.Default.DEV_ForceRouterHijacked)
                    {
                        if (string.IsNullOrEmpty(Properties.Settings.Default.DEV_ForceRouterHijackedUrlOverride))
                        {
                            _routerUrl = new Uri("http://" + _routerHost + ":12345");   // the port should not be open on the router
                        }
                        else
                        {
                            _routerUrl = new Uri(Properties.Settings.Default.DEV_ForceRouterHijackedUrlOverride);
                        }
                    }
                }
            }
            catch(Exception x)
            {
                _log.Error("Failed to parse router's presentation URL", x);

                // ignore
            }

            // check immediately
            CheckPresentation();
        }

        public void CheckPresentation()
        {
            lock (_checkPresentationLock)
            {
                try
                {
	                CheckPresentationInternal();
                }
                catch (Exception ex)
                {
                    _log.Error("Failed to check router presentation layer", ex);

                    throw;
                }
            }
        }

        private void CheckPresentationInternal()
        {
            if (string.IsNullOrEmpty(_routerHost) ||
                !Properties.Settings.Default.EnableRouterHijackingDetection)
            {
                _lastWorking = null;

                // ensure no notifications are shown
                if (_hijackedNotification.IsShown)
                {
                    _hijackedNotification.HideNotification();
                }

                // continue
                return;
            }

            // persist
            var router = _router;
            var host = _routerHost;
            var url = _routerUrl;

            // ping the host
            var pingSuccess = false;

            using (var p = new Ping())
            {
                var r = p.Send(
                    host,
                    Properties.Settings.Default.RouterHijackPingTimeoutMs
                    );

                pingSuccess = (r.Status == IPStatus.Success);
            }

            // if the host is offline, then
            // reset the last check timer
            if (!pingSuccess)
            {
                _log.Warn("Router cannot be pinged: " + host + "; router offline or ping denied?");

                _lastWorking = null;

                return;
            }

            // check presentation layer
            var connectSuccess = false;

            try
            {
	            using (var wc = new WebClient())
	            {
	                using (wc.OpenRead(url))
	                {
	                    connectSuccess = true;
	                }
	            }
            }
            catch (WebException ex)
            {
                _log.Warn(string.Format(
                    "Failed to connect router presentation URL: {0} ({1})", 
                    url.ToString(),
                    ex.Status
                    ), ex);

                // if we get an unauthorized
                // then the router asks the user for username/password
                // via HTTP basic auth.
                // we cannot handle that and hijacking detection should be disabled :-/
                var httpRes = (ex.Response as HttpWebResponse);

                if ((httpRes != null) &&
                    (httpRes.StatusCode == HttpStatusCode.Unauthorized))
                {
                    _log.Info("Router requires HTTP basic auth; disabling hijacking detection...");

                    // disable hijacking detection
                    DisableUnsupportedDetection();

                    // exit gently
                    return;
                }

                // ignore, this is expected
            }
            catch (Exception ex)
            {
                _log.Warn("Failed to connect router presentation URL: " + url.ToString(), ex);

                // ignore, this is expected
            }

            // conclude
            var now = DateTime.Now;

            if (connectSuccess)
            {
                _lastWorking = now;
            }
            else if (_lastWorking == null)
            {
                _lastWorking = DateTime.Now;
            }

            // notify user
            var timePassed = now.Subtract(_lastWorking.Value);

            if (timePassed > Properties.Settings.Default.RouterHijackTimeout)
            {
                // do not check if notification is already visible
                // because we want to update the time
                /*if (_hijackedNotification.IsShown)
                    return;*/

                // notify user!
                _hijackedNotification.Title = string.Format(
                    Properties.Resources.txtRouterHijackedNotificationTitle,
                    (int)timePassed.TotalMinutes,
                    router.Device.FriendlyName,
                    router.Device.UniqueDeviceName,
                    router.Device.Type,
                    Whs.Properties.Constants.RouterHijackedFaqUrl
                    );

                _hijackedNotification.Description = string.Format(
                    Properties.Resources.txtRouterHijackedNotificationDesc,
                    (int)timePassed.TotalMinutes,
                    router.Device.FriendlyName,
                    router.Device.UniqueDeviceName,
                    router.Device.Type,
                    Whs.Properties.Constants.RouterHijackedFaqUrl
                    );
                
                _hijackedNotification.UpdateNotifcation();
            }
            else
            {
                // is there a notification to hide?
                if (!_hijackedNotification.IsShown)
                    return;

                // hide then
                _hijackedNotification.HideNotification();
            }
        }

        private void DisableUnsupportedDetection()
        {
            var router = _router;

            // disable
            Properties.Settings.Default.EnableRouterHijackingDetection = false;

            Properties.Settings.Default.Save();

            // hide any other notification
            if (_hijackedNotification.IsShown)
                _hijackedNotification.HideNotification();

            // notify the user
            _unsupportedNotification.Title = string.Format(
                Properties.Resources.txtRouterHijackingUnsupportedNotificationTitle,
                router.Device.FriendlyName,
                router.Device.UniqueDeviceName,
                router.Device.Type,
                (int)Properties.Settings.Default.DefaultUserNotificationShowTime.TotalSeconds
                );

            _unsupportedNotification.Description = string.Format(
                Properties.Resources.txtRouterHijackingUnsupportedNotificationDesc,
                router.Device.FriendlyName,
                router.Device.UniqueDeviceName,
                router.Device.Type,
                (int)Properties.Settings.Default.DefaultUserNotificationShowTime.TotalSeconds
                );

            _unsupportedNotification.ShowNotifcation(Properties.Settings.Default.DefaultUserNotificationShowTime);
        }

        public RouterHijackingSlave()
        {
            _hijackedNotification = new Whs.WhsNotification(Whs.Properties.Constants.RouterHijackingNotificationId)
            {
                Severity = WHS_Notification_Severity.WHS_ERROR
            };

            _unsupportedNotification = new Whs.WhsNotification(Whs.Properties.Constants.RouterHijackingUnsupportedNotificationId)
            {
                Severity = WHS_Notification_Severity.WHS_WARNING
            };

            _checkTimer = new Timer(
                (o) => CheckPresentation(),
                null,
                1000,
                Properties.Settings.Default.RouterHijackCheckIntervalMs
                );
        }

    }
}
